/**
 * Authorizer
 *
 *
 * Copyright 2016 by Tjado Mäcke <tjado></tjado>@maecke.de>
 * Licensed under GNU General Public License 3.0.
 *
 * @license GPL-3.0 <https:></https:>//opensource.org/licenses/GPL-3.0>
 */
package com.seuic.hid_demo.util.hid

import android.bluetooth.*
import android.content.Context
import android.os.Build
import android.os.SystemClock
import androidx.annotation.RequiresApi
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import com.blankj.utilcode.util.SPUtils
import com.seuic.hid_demo.Constans
import com.seuic.hid_demo.util.ext.hidManager
import com.seuic.hid_demo.util.ext.isChineseCharUTF8
import com.seuic.hid_demo.util.ext.toHidList
import com.seuic.hid_demo.util.ext.toUnicodeHidList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.nio.charset.Charset
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.function.Consumer

@RequiresApi(Build.VERSION_CODES.P)
class OutputBluetoothKeyboard private constructor(ctx: Context) : OutputInterface {
    private val kbdKeyInterpreter: UsbHidKbd
    private val context: Context
    private val btAdapter: BluetoothAdapter?
    private var btDevice: BluetoothDevice? = null
    private var btHidDevice: BluetoothHidDevice? = null
    var connectState: MutableLiveData<Int?> = MutableLiveData()
    private var btOutputString: String? = null
    private var btOutputBytes: ByteArray? = null
    private val hidDescriptor = byteArrayOf( // Generated by HID Descriptor Tool
        // Generic HID descriptor
        // ... removed due to issues with Windows 10
        // Report descriptor - Keyboard
        0x05.toByte(), 0x01.toByte(),  // USAGE_PAGE (Generic Desktop)
        0x09.toByte(), 0x06.toByte(),  // USAGE (Keyboard)
        0xa1.toByte(), 0x01.toByte(),  // COLLECTION (Application)
        0x85.toByte(), HID_RID_KEYBOARD,  //   REPORT_ID (1)
        0x05.toByte(), 0x07.toByte(),  //   USAGE_PAGE (Keyboard)
        0x19.toByte(), 0xe0.toByte(),  //   USAGE_MINIMUM (Keyboard LeftControl)
        0x29.toByte(), 0xe7.toByte(),  //   USAGE_MAXIMUM (Keyboard Right GUI)
        0x15.toByte(), 0x00.toByte(),  //   LOGICAL_MINIMUM (0)
        0x25.toByte(), 0x01.toByte(),  //   LOGICAL_MAXIMUM (1)
        0x75.toByte(), 0x01.toByte(),  //   REPORT_SIZE (1)
        0x95.toByte(), 0x08.toByte(),  //   REPORT_COUNT (8)
        0x81.toByte(), 0x02.toByte(),  //   INPUT (Data,Var,Abs)
        0x95.toByte(), 0x01.toByte(),  //   REPORT_COUNT (1)
        0x75.toByte(), 0x08.toByte(),  //   REPORT_SIZE (8)
        0x81.toByte(), 0x03.toByte(),  //   INPUT (Cnst,Var,Abs)
        0x95.toByte(), 0x05.toByte(),  //   REPORT_COUNT (5)
        0x75.toByte(), 0x01.toByte(),  //   REPORT_SIZE (1)
        0x05.toByte(), 0x08.toByte(),  //   USAGE_PAGE (LEDs)
        0x19.toByte(), 0x01.toByte(),  //   USAGE_MINIMUM (Num Lock)
        0x29.toByte(), 0x05.toByte(),  //   USAGE_MAXIMUM (Kana)
        0x91.toByte(), 0x02.toByte(),  //   OUTPUT (Data,Var,Abs)
        0x95.toByte(), 0x01.toByte(),  //   REPORT_COUNT (1)
        0x75.toByte(), 0x03.toByte(),  //   REPORT_SIZE (3)
        0x91.toByte(), 0x03.toByte(),  //   OUTPUT (Cnst,Var,Abs)
        0x95.toByte(), 0x06.toByte(),  //   REPORT_COUNT (6)
        0x75.toByte(), 0x08.toByte(),  //   REPORT_SIZE (8)
        0x15.toByte(), 0x00.toByte(),  //   LOGICAL_MINIMUM (0)
        0x25.toByte(), 0x65.toByte(),  //   LOGICAL_MAXIMUM (101)
        0x05.toByte(), 0x07.toByte(),  //   USAGE_PAGE (Keyboard)
        0x19.toByte(), 0x00.toByte(),  //   USAGE_MINIMUM (Reserved (no event indicated))
        0x29.toByte(), 0x65.toByte(),  //   USAGE_MAXIMUM (Keyboard Application)
        0x81.toByte(), 0x00.toByte(),  //   INPUT (Data,Ary,Abs)
        0xc0.toByte(),  // END_COLLECTION
        // Report descriptor - Mouse
        0x05.toByte(), 0x01.toByte(),  // USAGE_PAGE (Generic Desktop)
        0x09.toByte(), 0x02.toByte(),  // USAGE (Mouse)
        0xa1.toByte(), 0x01.toByte(),  // COLLECTION (Application)
        0x85.toByte(), HID_RID_MOUSE,  //   REPORT_ID (1)
        0x09.toByte(), 0x01.toByte(),  //   USAGE (Pointer)
        0xa1.toByte(), 0x00.toByte(),  //   COLLECTION (Physical)
        0x05.toByte(), 0x09.toByte(),  //     USAGE_PAGE (Button)
        0x19.toByte(), 0x01.toByte(),  //     USAGE_MINIMUM (Button 1)
        0x29.toByte(), 0x03.toByte(),  //     USAGE_MAXIMUM (Button 3)
        0x15.toByte(), 0x00.toByte(),  //     LOGICAL_MINIMUM (0)
        0x25.toByte(), 0x01.toByte(),  //     LOGICAL_MAXIMUM (1)
        0x95.toByte(), 0x03.toByte(),  //     REPORT_COUNT (3)
        0x75.toByte(), 0x01.toByte(),  //     REPORT_SIZE (1)
        0x81.toByte(), 0x02.toByte(),  //     INPUT (Data,Var,Abs)
        0x95.toByte(), 0x01.toByte(),  //     REPORT_COUNT (1)
        0x75.toByte(), 0x05.toByte(),  //     REPORT_SIZE (5)
        0x81.toByte(), 0x03.toByte(),  //     INPUT (Cnst,Var,Abs)
        0x05.toByte(), 0x01.toByte(),  //     USAGE_PAGE (Generic Desktop)
        0x09.toByte(), 0x30.toByte(),  //     USAGE (X)
        0x09.toByte(), 0x31.toByte(),  //     USAGE (Y)
        0x15.toByte(), 0x81.toByte(),  //     LOGICAL_MINIMUM (-127)
        0x25.toByte(), 0x7f.toByte(),  //     LOGICAL_MAXIMUM (127)
        0x75.toByte(), 0x08.toByte(),  //     REPORT_SIZE (8)
        0x95.toByte(), 0x02.toByte(),  //     REPORT_COUNT (2)
        0x81.toByte(), 0x06.toByte(),  //     INPUT (Data,Var,Rel)
        0xc0.toByte(),  //   END_COLLECTION
        0xc0.toByte() // END_COLLECTION
    )

    private val inputReportQueue: Queue<ByteArray> = ConcurrentLinkedQueue<ByteArray>()


    override fun destruct() {
        deinitializeBluetoothHidDevice()
    }

    fun checkBluetoothStatus(): Boolean {
        return btAdapter?.isEnabled ?: false
    }

    fun initializeBluetoothHidDevice() {
        Utilities.dbginfo(TAG, "initializeBluetoothHidDevice")
        val btHidDeviceSqp = BluetoothHidDeviceAppSdpSettings(
            "Seuic Bluetooth Hid",
            "Seuic Bluetooth Hid",
            "suciv.hid",
            BluetoothHidDevice.SUBCLASS1_COMBO,
            hidDescriptor
        )
        val serviceListener: BluetoothProfile.ServiceListener =
            object : BluetoothProfile.ServiceListener {
                override fun onServiceConnected(profile: Int, btHidDeviceProxy: BluetoothProfile) {
                    if (profile == BluetoothProfile.HID_DEVICE) {
                        Utilities.dbginfo(TAG, "onServiceConnected: HID_DEVICE")
                        btHidDevice = btHidDeviceProxy as BluetoothHidDevice
                        btHidDevice!!.registerApp(
                            btHidDeviceSqp,
                            null,
                            null,
                            { obj: Runnable -> obj.run() },
                            callback
                        )
                    }
                }

                override fun onServiceDisconnected(profile: Int) {
                    if (profile == BluetoothProfile.HID_DEVICE) {
                        Utilities.dbginfo(TAG, "onServiceDisconnected: HID_DEVICE")
                    }
                }
            }
        btAdapter!!.getProfileProxy(context, serviceListener, BluetoothProfile.HID_DEVICE)
    }

    fun deinitializeBluetoothHidDevice() {
        Utilities.dbginfo(TAG, "deinitializeBluetoothHidDevice")

        // disconnect not necessary as unregisterApp will do it for us...
        //if(btDevice != null) {
        //    Utilities.dbginfo(TAG,"disconnect: " + btDevice);
        //    btHidDevice.disconnect(btDevice);
        //}
        if (btHidDevice != null) {
            Utilities.dbginfo(TAG, "unregisterApp")
            btHidDevice!!.unregisterApp()
            Utilities.dbginfo(TAG, "closeProfileProxy")
            btAdapter!!.closeProfileProxy(BluetoothProfile.HID_DEVICE, btHidDevice)
        }
    }

    private val callback: BluetoothHidDevice.Callback = object : BluetoothHidDevice.Callback() {
        override fun onConnectionStateChanged(device: BluetoothDevice, state: Int) {
            Utilities.dbginfo(TAG, "onConnectionStateChanged - $device:$state")
            if (device == btDevice) {
                connectState.postValue(state)
                when (state) {
                    BluetoothProfile.STATE_CONNECTED -> {
                        Utilities.dbginfo(TAG, "onConnectionStateChanged: CONNECTED")

                        // delay required otherwise the device is not ready yet to receive the data
                        SystemClock.sleep(300)

                        // connection ready ... send data to device
                        send()
                    }
                    BluetoothProfile.STATE_CONNECTING -> Utilities.dbginfo(
                        TAG,
                        "onConnectionStateChanged: CONNECTING"
                    )
                    BluetoothProfile.STATE_DISCONNECTING -> Utilities.dbginfo(
                        TAG,
                        "onConnectionStateChanged: DISCONNECTING"
                    )
                    BluetoothProfile.STATE_DISCONNECTED -> Utilities.dbginfo(
                        TAG,
                        "onConnectionStateChanged: DISCONNECTED"
                    )
                }
            }
        }
    }
    val bondedDevices: Set<BluetoothDevice>
        get() = if (btAdapter != null) {
            btAdapter.bondedDevices
        } else HashSet()

    fun connectDevice() {
        btAdapter!!.bondedDevices.forEach(Consumer { device: BluetoothDevice ->
            if (device.bondState == BluetoothDevice.BOND_BONDED) {
                Utilities.dbginfo(TAG, "connectDeviceAndSend: $device")
                btDevice = device
                val state = btHidDevice!!.getConnectionState(device)
                if (state != BluetoothProfile.STATE_CONNECTING && state != BluetoothProfile.STATE_CONNECTED) {
                    btHidDevice!!.connect(device)
                }
            }
        })
    }

    fun connectDevice(device: BluetoothDevice?) {
        Utilities.dbginfo(TAG, "connectDeviceAndSend: $device")
        if (device != null) {
            btDevice = device
            val state = btHidDevice!!.getConnectionState(device)
            if (state != BluetoothProfile.STATE_CONNECTING && state != BluetoothProfile.STATE_CONNECTED) {
                btHidDevice!!.connect(device)
            }
        }
    }

    fun connectDeviceAndSend(device: BluetoothDevice?, output: String?) {
        Utilities.dbginfo(TAG, "connectDeviceAndSend: $device")
        if (device != null) {
            btDevice = device
            btOutputString = output
            val state = btHidDevice!!.getConnectionState(device)
            if (state != BluetoothProfile.STATE_CONNECTING && state != BluetoothProfile.STATE_CONNECTED) {
                btHidDevice!!.connect(device)
            } else {
                send()
            }
        }
    }

    fun connectDeviceAndSend(device: BluetoothDevice?, output: ByteArray) {
        Utilities.dbginfo(TAG, "connectDeviceAndSend: $device")
        if (device != null) {
            btDevice = device
            btOutputBytes = output
            if (output.size % 8 != 0) {
                Utilities.dbginfo(TAG, "connectDeviceAndSend: MOD ERROR: " + output.size)
                return
            }
            val state = btHidDevice!!.getConnectionState(device)
            if (state != BluetoothProfile.STATE_CONNECTING && state != BluetoothProfile.STATE_CONNECTED) {
                btHidDevice!!.connect(device)
            } else if (state == BluetoothProfile.STATE_CONNECTING || state == BluetoothProfile.STATE_DISCONNECTING) {
                Utilities.dbginfo(TAG, "connectDeviceAndSend: CONNECTING/DISCONNECTING... abort")
            } else {
                send()
            }
        }
    }

    private fun send() {
        Utilities.dbginfo(TAG, "send")
        try {
            if (btOutputBytes != null) {
                val blockSize = 8
                val blockCount = btOutputBytes!!.size / blockSize
                var start = 0
                for (i in 0 until blockCount) {
                    val scancode = Arrays.copyOfRange(btOutputBytes, start, start + blockSize)
                    Utilities.dbginfo(TAG, "send: " + Utilities.bytesToHex(scancode))
                    sendReport(scancode)
                    clean()
                    start += blockSize
                }
                btOutputBytes = null
            } else if (btOutputString != null) {
                sendText(btOutputString!!)
                btOutputString = null
            }
        } catch (e: IOException) {
            Utilities.dbginfo(TAG, "send error: " + e.localizedMessage)
            e.printStackTrace()
        }
    }

    private fun sendReport(scancode: ByteArray) {
        inputReportQueue.offer(scancode)
    }


    @Throws(IOException::class)
    private fun clean() {
        // overwriting the last keystroke, otherwise it will be repeated until the next writing
        // and it would not be possible to repeat the keystroke
        val scancode_reset = kbdKeyInterpreter.getScancode(null)
        Utilities.dbginfo(TAG, "RST > " + Utilities.bytesToHex(scancode_reset))
        sendReport(scancode_reset)
    }

    fun convertTextToScancode(text: String): ByteArray {
        val outputStream = ByteArrayOutputStream()
        for (i in 0 until text.length) {
            val textCharString = text[i].toString()
            try {
                val scancode = kbdKeyInterpreter.getScancode(textCharString)
                Utilities.dbginfo(
                    TAG, "convertTextToScancode: '$textCharString' > " + Utilities
                        .bytesToHex(scancode)
                )
                outputStream.write(scancode)
            } catch (e: NoSuchElementException) {
                Utilities.dbginfo(TAG, "'$textCharString' mapping not found")
            } catch (e: IOException) {
                e.printStackTrace()
                Utilities.dbginfo(TAG, "convertText:" + e.localizedMessage)
            }
        }
        return outputStream.toByteArray()
    }

    @Throws(IOException::class)
    @Synchronized
    override fun sendText(output: String, charset: Charset, isUnicode: Boolean): Boolean {
        var scancode: ByteArray
        for (element in output) {
            if (element.isChineseCharUTF8()) {
                val byteArrays = if (!isUnicode) {
                    element.toString().toHidList(charset)
                } else {
                    element.toString().toUnicodeHidList()
                }
                byteArrays.forEach { numberStr ->
                    kbdKeyInterpreter.getScancode(if (numberStr in "0".."9") numberStr + "_keypad" else numberStr)
                        .takeIf { it.isNotEmpty() }?.run {
                            this.copyOf().apply {
                                this[0] = 0x04
                            }.also {
                                sendReport(it)
                                Utilities.dbginfo(
                                    TAG,
                                    "Chinese:$element ***$numberStr***${it.contentToString()}"
                                )
                            }
                        }
                }
                clean()

            } else {
                val textCharString = element.toString()
                scancode = kbdKeyInterpreter.getScancode(textCharString)
                Utilities.dbginfo(
                    TAG, "'$textCharString' > " + Utilities
                        .bytesToHex(scancode)
                )
                sendReport(scancode)
                clean()
            }
        }
        return true
    }

    @Throws(IOException::class)
    fun sendSingleKey(keyName: String): Boolean {
        val scancode: ByteArray
        try {
            scancode = kbdKeyInterpreter.getScancode(keyName)
            Utilities.dbginfo(
                TAG, "'$keyName' > " + Utilities
                    .bytesToHex(scancode)
            )
            sendReport(scancode)
            clean()
        } catch (e: NoSuchElementException) {
            Utilities.dbginfo(TAG, "'$keyName' mapping not found")
            return false
        }
        return true
    }

    @Throws(IOException::class)
    override fun sendReturn(): Boolean {
        return sendSingleKey("return")
    }

    @Throws(IOException::class)
    override fun sendTabulator(): Boolean {
        return sendSingleKey("tabulator")
    }

    @Throws(IOException::class)
    fun getSingleKey(keyName: String): ByteArray {
        var scancode: ByteArray
        try {
            return kbdKeyInterpreter.getScancode(keyName)
        } catch (e: NoSuchElementException) {
            Utilities.dbginfo(TAG, "'$keyName' mapping not found")
        }
        return ByteArray(0)
    }

    @get:Throws(IOException::class)
    val `return`: ByteArray
        get() = getSingleKey("return")

    @get:Throws(IOException::class)
    val tabulator: ByteArray
        get() = getSingleKey("tabulator")

    @Throws(FileNotFoundException::class, IOException::class)
    fun sendScancode(output: ByteArray) {
        if (output.size == 8 || output.size == 2) {
            Utilities.dbginfo(TAG, Utilities.bytesToHex(output))
            sendReport(output)
            clean()
        } else if (output.size == 1) {
            val scancode = byteArrayOf(0x00, 0x00, output[0], 0x00, 0x00, 0x00, 0x00, 0x00)
            Utilities.dbginfo(TAG, Utilities.bytesToHex(scancode))
            sendReport(scancode)
            clean()
        }
    }

    val isConnected: Boolean
        get() = connectState.value != null && connectState.value == BluetoothProfile.STATE_CONNECTED

    companion object {
        private const val TAG = "OutputBluetoothKeyboard"
        const val HID_RID_MOUSE: Byte = 0x01
        const val HID_RID_KEYBOARD: Byte = 0x02
        private lateinit var context: Context
        private var outputBluetoothKeyboard: OutputBluetoothKeyboard? = null

        fun getInstance(ctx: Context): OutputBluetoothKeyboard {
            if (outputBluetoothKeyboard == null) {
                synchronized(SPUtils::class.java) {
                    if (outputBluetoothKeyboard == null) {
                        outputBluetoothKeyboard = OutputBluetoothKeyboard(ctx)
                    }
                }
            }
            return outputBluetoothKeyboard!!
        }
    }

    init {
        kbdKeyInterpreter = UsbHidKbd_en_US()
        btAdapter = BluetoothAdapter.getDefaultAdapter()
        context = ctx
        MainScope().launch(Dispatchers.IO) {
            do {
                inputReportQueue.poll()?.also { code ->
                    val state = btHidDevice!!.sendReport(btDevice, HID_RID_KEYBOARD.toInt(), code)
                    Utilities.dbginfo(TAG, "send state: $state")
                    delay(50)
                }
            } while (true)
        }
    }
}